﻿// Created by Zone Five Software
// http://www.zonefivesoftware.com
// Copyright (c) 2011

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;

namespace Zeo
{
    /// <summary>
    /// Wrapper class for Zeo API. Refer to http://mysleep.myzeo.com/api/api.shtml
    /// </summary>
    public class ZeoApi : IDisposable
    {
        /// <summary>
        /// Connect to the service via basic authentication
        /// </summary>
        /// <param name="apiKey">Developer API key</param>
        /// <param name="username">Username</param>
        /// <param name="password">Password</param>
        /// <returns></returns>
        public static ZeoApi ConnectHttpBasicAuthentication(string apiKey, string username, string password)
        {
            return new ZeoApi(apiKey, username, password);
        }

        /// <summary>
        /// Service call status
        /// </summary>
        public enum Status
        {
            Success = 0,
            Error_Unknown = 1,
            Error_BadParam = 2,
            Error_NoAccount = 3,
            Error_NoAccountForSession = 4,
            Error_MissingApiKey = 5,
            Error_ApiDomainMismatch = 6,
            Error_BadApiKey = 7,
            Error_ExceededApiLimit = 8,
        }

        /// <summary>
        /// Exception class for service calls. Note that other exceptions (such as WebException) may occur: catch them all.
        /// </summary>
        public class ApiException : Exception
        {
            public ApiException(Status status, XmlDocument xmlDoc)
            {
                this.status = status;
                try
                {
                    errorMessage = xmlDoc.DocumentElement.SelectSingleNode("errMsg").InnerText;
                }
                catch (Exception) { }
            }

            public Status Status
            {
                get { return status; }
            }

            public string ErrorMessage
            {
                get { return errorMessage; }
            }

            private Status status;
            private string errorMessage;
        }

        /// <summary>
        /// The URL which matches the API key. This is option if used on a webserver.
        /// </summary>
        public string RefererUrl
        {
            get { return refererUrl; }
            set { refererUrl = value; }
        }

        /// <summary>
        /// If true, trace debug output for all request/response calls
        /// </summary>
        public bool DebugTrace
        {
            get { return debugTrace; }
            set { debugTrace = value; }
        }

        public double GetOverallAverageZQScore()
        {
            return GetOverallDoubleScore(CreateUrl(apiActions.getOverallAverageZQScore));
        }

        public double GetOverallAverageDayFeelScore()
        {
            return GetOverallDoubleScore(CreateUrl(apiActions.getOverallAverageDayFeelScore));
        }

        public double GetOverallAverageMorningFeelScore()
        {
            return GetOverallDoubleScore(CreateUrl(apiActions.getOverallAverageMorningFeelScore));
        }

        public double GetOverallAverageSleepStealerScore()
        {
            return GetOverallDoubleScore(CreateUrl(apiActions.getOverallAverageSleepStealerScore));
        }

        public IList<DateTime> GetAllDatesWithSleepData()
        {
            return GetDateList(CreateUrl(apiActions.getAllDatesWithSleepData));
        }

        public IList<DateTime> GetDatesWithSleepDataInRange(DateTime? from, DateTime? to)
        {
            string url = CreateUrl(apiActions.getDatesWithSleepDataInRange);
            if (from.HasValue) url += "&dateFrom=" + FormatDate(from.Value);
            if (to.HasValue) url += "&dateTo=" + FormatDate(to.Value);
            return GetDateList(url);
        }

        public SleepStats GetSleepStatsForDate(DateTime date)
        {
            SleepStats data = null;

            string url = CreateUrl(apiActions.getSleepStatsForDate);
            url += "&date=" + FormatDate(date);
            string response = ReadToEnd(CreateWebRequest(url));
            OutputDebugTrace(url, response);
            XmlDocument xmlDoc = LoadXmlAndCheckStatus(response);
            XmlNode statsNode = xmlDoc.DocumentElement.SelectSingleNode(xmlTags.sleepStats);
            if (statsNode != null)
            {
                data = new SleepStats();
                ReadSleepStats(statsNode, data);
            }
            return data;
        }

        public SleepRecord GetSleepRecordForDate(DateTime date)
        {
            SleepRecord data = null;

            string url = CreateUrl(apiActions.getSleepRecordForDate);
            url += "&date=" + FormatDate(date);
            string response = ReadToEnd(CreateWebRequest(url));
            OutputDebugTrace(url, response);
            XmlDocument xmlDoc = LoadXmlAndCheckStatus(response);
            XmlNode statsNode = xmlDoc.DocumentElement.SelectSingleNode(xmlTags.sleepRecord);
            if (statsNode != null)
            {
                data = new SleepRecord();
                // TODO: Reading sleep record properties implemented yet.
                ReadSleepStats(statsNode, data);
            }
            return data;
        }

        public void Logout()
        {
            ReadToEnd(CreateWebRequest(CreateUrl(apiActions.logout)));
        }

        private ZeoApi(string apiKey, string username, string password)
        {
            this.apiKey = apiKey;
            this.username = username;
            this.password = password;
        }

        public void Dispose()
        {
            Logout();
        }

        private string CreateUrl(string actionName)
        {
            string url = string.Format(baseUrl, apiVersion, apiResponseType, apiServiceName, actionName, apiKey);
            return url;
        }

        private WebRequest CreateWebRequest(string url)
        {
            WebRequest request = WebRequest.Create(url);

            string usernamePassword = username + ":" + password;
            CredentialCache credentials = new CredentialCache();
            credentials.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
            request.Credentials = credentials;
            request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(new ASCIIEncoding().GetBytes(usernamePassword)));
            if (refererUrl != null) ((HttpWebRequest)request).Referer = refererUrl;

            return request;
        }

        private string ReadToEnd(WebRequest request)
        {
            WebResponse response = request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader reader = new StreamReader(stream, Encoding.UTF8);
            return reader.ReadToEnd();
        }

        private XmlDocument LoadXmlAndCheckStatus(string response)
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(response);
            Status status = ReadStatus(xmlDoc);
            if (status != Status.Success) throw new ApiException(status, xmlDoc);
            return xmlDoc;
        }

        private Status ReadStatus(XmlDocument responseDoc)
        {
            try
            {
                int statusValue = XmlConvert.ToInt32(responseDoc.DocumentElement.GetAttribute(xmlTags.status));
                return (Status)statusValue;
            }
            catch (Exception) { }
            return Status.Error_Unknown;
        }

        private double GetResponseValue(XmlDocument responseDoc, double defaultValue)
        {
            return GetNodeValue(responseDoc.DocumentElement.SelectSingleNode(xmlTags.value), defaultValue);
        }

        private double GetOverallDoubleScore(string url)
        {
            string response = ReadToEnd(CreateWebRequest(url));
            OutputDebugTrace(url, response);
            XmlDocument xmlDoc = LoadXmlAndCheckStatus(response);
            return GetResponseValue(xmlDoc, -1);
        }

        private IList<DateTime> GetDateList(string url)
        {
            string response = ReadToEnd(CreateWebRequest(url));
            OutputDebugTrace(url, response);

            XmlDocument xmlDoc = LoadXmlAndCheckStatus(response);
            IList<DateTime> dates = new List<DateTime>();
            DateTime defaultDate = new DateTime(1, 1, 1, 0, 0, 0);
            foreach (XmlNode dateNode in xmlDoc.DocumentElement.SelectNodes(xmlTags.dateList + "/" + xmlTags.date))
            {
                DateTime date = GetNodeDateTime(dateNode, defaultDate);
                if (date.Year != 1) dates.Add(date.Date);
            }
            return dates;
        }

        private void ReadSleepStats(XmlNode statsNode, SleepStats stats)
        {
            XmlNode attr;
            stats.NumAwakenings = (int)GetNodeValue(statsNode.SelectSingleNode(xmlTags.awakenings), stats.NumAwakenings);
            stats.AwakeningsZQPoints = GetNodeValue(statsNode.SelectSingleNode(xmlTags.awakeningsZqPoints), stats.AwakeningsZQPoints);
            stats.BedTime = GetNodeDateTime(statsNode.SelectSingleNode(xmlTags.bedTime), stats.BedTime);
            attr = statsNode.SelectSingleNode(xmlTags.grouping);
            if (attr != null && attr.InnerText.Length > 0)
            {
                try
                {
                    stats.Grouping = (SleepStats.SleepStatsGrouping)Enum.Parse(typeof(SleepStats.SleepStatsGrouping),attr.InnerText);
                }
                catch { }
            }
            stats.MorningFeel = (int)GetNodeValue(statsNode.SelectSingleNode(xmlTags.morningFeel), stats.MorningFeel);
            stats.RiseTime = GetNodeDateTime(statsNode.SelectSingleNode(xmlTags.riseTime), stats.RiseTime);
            stats.StartDate = GetNodeDateTime(statsNode.SelectSingleNode(xmlTags.startDate), stats.StartDate);

            GetNodeStageInfo(stats.DeepStageInfo, statsNode.SelectSingleNode(xmlTags.timeInDeep), statsNode.SelectSingleNode(xmlTags.timeInDeepPercentage), statsNode.SelectSingleNode(xmlTags.timeInDeepZqPoints));
            GetNodeStageInfo(stats.LightStageInfo, statsNode.SelectSingleNode(xmlTags.timeInLight), statsNode.SelectSingleNode(xmlTags.timeInLightPercentage), statsNode.SelectSingleNode(xmlTags.timeInLightZqPoints));
            GetNodeStageInfo(stats.RemStageInfo, statsNode.SelectSingleNode(xmlTags.timeInRem), statsNode.SelectSingleNode(xmlTags.timeInRemPercentage), statsNode.SelectSingleNode(xmlTags.timeInRemZqPoints));
            GetNodeStageInfo(stats.WakeStageInfo, statsNode.SelectSingleNode(xmlTags.timeInWake), statsNode.SelectSingleNode(xmlTags.timeInWakePercentage), statsNode.SelectSingleNode(xmlTags.timeInWakeZqPoints));

            stats.TimeToSleep = GetNodeTimeSpanFromMinutesValue(statsNode.SelectSingleNode(xmlTags.timeToZ), stats.TimeToSleep);
            stats.TotalSleep = GetNodeTimeSpanFromMinutesValue(statsNode.SelectSingleNode(xmlTags.totalZ), stats.TotalSleep);
            stats.TotalSleepZQPoints = GetNodeValue(statsNode.SelectSingleNode(xmlTags.totalZZqPoints), stats.TotalSleepZQPoints);
            stats.TotalZQScore = GetNodeValue(statsNode.SelectSingleNode(xmlTags.zq), stats.TotalZQScore);
        }

        private string FormatDate(DateTime date)
        {
            return date.ToString("yyyy-MM-dd");
        }

        private double GetNodeValue(XmlNode node, double defaultValue)
        {
            if (node != null)
            {
                try
                {
                    return XmlConvert.ToDouble(node.InnerText);
                }
                catch { }
            }
            return defaultValue;
        }

        private DateTime GetNodeDateTime(XmlNode node, DateTime defaultValue)
        {
            if (node != null)
            {
                try
                {
                    int year = XmlConvert.ToInt32(node.SelectSingleNode(xmlTags.year).InnerText);
                    int month = XmlConvert.ToInt32(node.SelectSingleNode(xmlTags.month).InnerText);
                    int day = XmlConvert.ToInt32(node.SelectSingleNode(xmlTags.day).InnerText);
                    XmlNode attr;
                    attr = node.SelectSingleNode(xmlTags.hour);
                    int hour = attr != null && attr.InnerText.Length > 0 ? XmlConvert.ToInt32(attr.InnerText) : defaultValue.Hour;
                    attr = node.SelectSingleNode(xmlTags.minute);
                    int minute = attr != null && attr.InnerText.Length > 0 ? XmlConvert.ToInt32(attr.InnerText) : defaultValue.Minute;
                    attr = node.SelectSingleNode(xmlTags.second);
                    int second = attr != null && attr.InnerText.Length > 0 ? XmlConvert.ToInt32(attr.InnerText) : defaultValue.Second;
                    return new DateTime(year, month, day, hour, minute, second);
                }
                catch { }
            }
            return defaultValue;
        }

        private TimeSpan GetNodeTimeSpanFromMinutesValue(XmlNode node, TimeSpan defaultValue)
        {
            try
            {
                return TimeSpan.FromMinutes(GetNodeValue(node, defaultValue.TotalMinutes));
            }
            catch { }
            return defaultValue;
        }

        private void GetNodeStageInfo(SleepStats.SleepStageInfo info, XmlNode time, XmlNode percent, XmlNode zqPoints)
        {
            info.Time = GetNodeTimeSpanFromMinutesValue(time, info.Time);
            info.TimePercent = GetNodeValue(percent, info.TimePercent);
            info.ZQPoints = GetNodeValue(zqPoints, info.ZQPoints);
        }

        private void OutputDebugTrace(string url, string response)
        {
            if (!debugTrace) return;
            System.Diagnostics.Trace.WriteLine("REQUEST: " + url);
            System.Diagnostics.Trace.WriteLine("RESPONSE:");
            System.Diagnostics.Trace.WriteLine(response);
        }

        private string apiKey;
        private string username;
        private string password;
        private string refererUrl = null;
        private bool debugTrace = false;

        private static string baseUrl = "https://api.myzeo.com:8443/zeows/api/{0}/{1}/{2}/{3}?key={4}";

        private static string apiVersion = "v1";
        private static string apiResponseType = "xml";
        private static string apiServiceName = "sleeperService";

        private class apiActions
        {
            public static string getOverallAverageZQScore = "getOverallAverageZQScore";
            public static string getOverallAverageDayFeelScore = "getOverallAverageDayFeelScore";
            public static string getOverallAverageMorningFeelScore = "getOverallAverageMorningFeelScore";
            public static string getOverallAverageSleepStealerScore = "getOverallAverageSleepStealerScore";

            public static string getAllDatesWithSleepData = "getAllDatesWithSleepData";
            public static string getDatesWithSleepDataInRange = "getDatesWithSleepDataInRange";
            public static string getSleepStatsForDate = "getSleepStatsForDate";
            public static string getSleepRecordForDate = "getSleepRecordForDate";

            public static string logout = "logout";
        }

        private class xmlTags
        {
            public static string status = "status";
            public static string value = "value";

            public static string dateList = "dateList";
            public static string date = "date";
            public static string year = "year";
            public static string month = "month";
            public static string day = "day";
            public static string hour = "hour";
            public static string minute = "minute";
            public static string second = "second";

            public static string sleepStats = "sleepStats";
            public static string sleepRecord = "sleepRecord";

            public static string awakenings = "awakenings";
            public static string awakeningsZqPoints = "awakeningsZqPoints";
            public static string bedTime = "bedTime";
            public static string grouping = "grouping";
            public static string morningFeel = "morningFeel";
            public static string riseTime = "riseTime";
            public static string startDate = "startDate";
            public static string timeInDeep = "timeInDeep";
            public static string timeInDeepPercentage = "timeInDeepPercentage";
            public static string timeInDeepZqPoints = "timeInDeepZqPoints";
            public static string timeInLight = "timeInLight";
            public static string timeInLightPercentage = "timeInLightPercentage";
            public static string timeInLightZqPoints = "timeInLightZqPoints";
            public static string timeInRem = "timeInRem";
            public static string timeInRemPercentage = "timeInRemPercentage";
            public static string timeInRemZqPoints = "timeInRemZqPoints";
            public static string timeInWake = "timeInWake";
            public static string timeInWakePercentage = "timeInWakePercentage";
            public static string timeInWakeZqPoints = "timeInWakeZqPoints";
            public static string timeToZ = "timeToZ";
            public static string totalZ = "totalZ";
            public static string totalZZqPoints = "totalZZqPoints";
            public static string zq = "zq";
        }
    }
}
